Bagikan melalui


Membuat Profil Panggilan API Direct3D secara akurat (Direct3D 9)

Setelah Anda memiliki aplikasi Microsoft Direct3D fungsi dan ingin meningkatkan performanya, Anda umumnya menggunakan alat pembuatan profil di luar rak atau beberapa teknik pengukuran kustom untuk mengukur waktu yang diperlukan untuk menjalankan satu atau beberapa panggilan antarmuka pemrograman aplikasi (API). Jika Anda telah melakukan ini tetapi mendapatkan hasil pengaturan waktu yang bervariasi dari satu urutan render ke urutan berikutnya, atau Anda membuat hipotesis yang tidak menyimpan hasil eksperimen aktual, informasi berikut dapat membantu Anda memahami alasannya.

Informasi yang diberikan di sini didasarkan pada asumsi bahwa Anda memiliki pengetahuan dan pengalaman dengan hal-hal berikut:

  • Pemrograman C/C++
  • Pemrograman API Direct3D
  • Mengukur waktu API
  • Kartu video dan driver perangkat lunaknya
  • Kemungkinan hasil yang tidak dapat dijelaskan dari pengalaman pembuatan profil sebelumnya

Pembuatan profil Direct3D secara akurat sulit

Profiler melaporkan jumlah waktu yang dihabiskan di setiap panggilan API. Hal ini dilakukan untuk meningkatkan performa dengan menemukan dan menyetel hot spot. Ada berbagai jenis profiler dan teknik pembuatan profil.

  • Profiler pengambilan sampel duduk diam banyak waktu, terbangun pada interval tertentu untuk sampel (atau untuk merekam) fungsi yang dijalankan. Ini mengembalikan persentase waktu yang dihabiskan dalam setiap panggilan. Umumnya, profiler pengambilan sampel tidak terlalu invasif terhadap aplikasi dan memiliki dampak minimal pada overhead untuk aplikasi.
  • Profiler instrumenting mengukur waktu aktual yang diperlukan agar panggilan kembali. Ini membutuhkan kompilasi memulai dan menghentikan pemisah ke dalam aplikasi. Profiler instrumenting secara komparatif lebih invasif terhadap aplikasi daripada profiler pengambilan sampel.
  • Dimungkinkan juga untuk menggunakan teknik pembuatan profil kustom dengan timer berkinerja tinggi. Ini menghasilkan hasil yang sangat mirip dengan profiler instrumenting.

Jenis profiler atau teknik pembuatan profil yang digunakan hanyalah bagian dari tantangan menghasilkan pengukuran yang akurat.

Pembuatan profil memberi Anda jawaban yang membantu Anda menganggarkan performa. Misalnya, Anda tahu bahwa panggilan API rata-rata seribu siklus jam untuk dijalankan. Anda dapat menegaskan beberapa kesimpulan tentang performa seperti berikut:

  • CPU 2 GHz (yang menghabiskan 50 persen dari rendering waktunya) terbatas untuk memanggil API ini 1 juta kali sedetik.
  • Untuk mencapai 30 bingkai per detik, Anda tidak dapat memanggil API ini lebih dari 33.000 kali per bingkai.
  • Anda hanya dapat merender objek 3,3K per bingkai (dengan asumsi 10 panggilan API ini untuk setiap urutan render objek).

Dengan kata lain, jika Anda memiliki cukup waktu per panggilan API, Anda dapat menjawab pertanyaan penganggaran seperti jumlah primitif yang dapat dirender secara interaktif. Tetapi angka mentah yang dikembalikan oleh profiler instrumenting tidak akan menjawab pertanyaan penganggahan secara akurat. Ini karena alur grafis memiliki masalah desain yang kompleks seperti jumlah komponen yang perlu melakukan pekerjaan, jumlah prosesor yang mengontrol bagaimana alur kerja antara komponen, dan strategi pengoptimalan yang diterapkan dalam runtime dan pada driver yang dirancang untuk membuat alur lebih efisien.

Setiap Panggilan API Melewati Beberapa Komponen

Setiap panggilan diproses oleh beberapa komponen dalam perjalanan dari aplikasi ke kartu video. Misalnya, pertimbangkan urutan render berikut yang berisi dua panggilan untuk menggambar satu segitiga:

SetTexture(...);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

Diagram konseptual berikut menunjukkan berbagai komponen yang harus dilewati panggilan.

diagram of graphics components that api calls go through

Aplikasi memanggil Direct3D yang mengontrol adegan, menangani interaksi pengguna, dan menentukan bagaimana penyajian dilakukan. Semua pekerjaan ini ditentukan dalam urutan render, yang dikirim ke runtime menggunakan panggilan API Direct3D. Urutan render secara virtual independen perangkat keras (yaitu, panggilan API independen perangkat keras tetapi aplikasi memiliki pengetahuan tentang fitur apa yang didukung kartu video).

Runtime mengonversi panggilan ini menjadi format independen perangkat. Runtime menangani semua komunikasi antara aplikasi dan driver, sehingga aplikasi akan berjalan pada lebih dari satu perangkat keras yang kompatibel (tergantung pada fitur yang diperlukan). Saat mengukur panggilan fungsi, profiler instrumenting mengukur waktu yang dihabiskannya dalam fungsi serta waktu untuk fungsi kembali. Salah satu batasan profiler instrumenting adalah mungkin tidak termasuk waktu yang dibutuhkan pengemudi untuk mengirim pekerjaan yang dihasilkan ke kartu video atau waktu bagi kartu video untuk memproses pekerjaan. Dengan kata lain, profiler instrumenting off-the-shelf gagal mengaitkan semua pekerjaan yang terkait dengan setiap panggilan fungsi.

Driver perangkat lunak menggunakan pengetahuan khusus perangkat keras tentang kartu video untuk mengonversi perintah independen perangkat menjadi urutan perintah kartu video. Driver juga dapat mengoptimalkan urutan perintah yang dikirim ke kartu video, sehingga penyajian pada kartu video dilakukan secara efisien. Pengoptimalan ini dapat menyebabkan masalah pembuatan profil karena jumlah pekerjaan yang dilakukan tidak seperti yang terlihat (Anda mungkin perlu memahami pengoptimalan untuk memperhitungkannya). Driver biasanya mengembalikan kontrol ke runtime sebelum kartu video selesai memproses semua perintah.

Kartu video melakukan sebagian besar penyajian dengan menggabungkan data dari buffer puncak dan indeks, tekstur, informasi status render, dan perintah grafis. Ketika kartu video selesai dirender, pekerjaan yang dibuat dari urutan render selesai.

Setiap panggilan API Direct3D harus diproses oleh setiap komponen (runtime, driver, dan kartu video) untuk merender apa pun.

Ada Lebih dari Satu Prosesor yang Mengontrol Komponen

Hubungan antara komponen-komponen ini bahkan lebih kompleks, karena aplikasi, runtime, dan driver dikontrol oleh satu prosesor dan kartu video dikendalikan oleh prosesor terpisah. Diagram berikut menunjukkan dua jenis prosesor: unit pemrosesan pusat (CPU) dan unit pemrosesan grafis (GPU).

diagram of a cpu and a gpu and their components

Sistem PC memiliki setidaknya satu CPU dan satu GPU, tetapi dapat memiliki lebih dari satu atau keduanya. CPU terletak di motherboard, dan GPU terletak baik di motherboard atau di kartu video. Kecepatan CPU ditentukan oleh chip jam pada motherboard, dan kecepatan GPU ditentukan oleh chip jam terpisah. Jam CPU mengontrol kecepatan pekerjaan yang dilakukan oleh aplikasi, runtime, dan driver. Aplikasi mengirimkan pekerjaan ke GPU melalui runtime dan driver.

CPU dan GPU umumnya berjalan pada kecepatan yang berbeda, terpisah satu sama lain. GPU dapat menanggapi pekerjaan segera setelah pekerjaan tersedia (dengan asumsi GPU telah selesai memproses pekerjaan sebelumnya). Pekerjaan GPU dilakukan secara paralel dengan pekerjaan CPU seperti yang disorot oleh garis melengkung pada gambar di atas. Profiler umumnya mengukur performa CPU, bukan GPU. Ini membuat pembuatan profil menantang, karena pengukuran yang dilakukan oleh profiler instrumenting mencakup waktu CPU tetapi mungkin tidak termasuk waktu GPU.

Tujuan GPU adalah untuk pemrosesan off-load dari CPU ke prosesor yang dirancang khusus untuk pekerjaan grafis. Pada kartu video modern, GPU menggantikan banyak pekerjaan transformasi dan pencahayaan dalam alur dari CPU ke GPU. Ini sangat mengurangi beban kerja CPU, meninggalkan lebih banyak siklus CPU yang tersedia untuk pemrosesan lain. Untuk menyetel aplikasi grafis untuk performa puncak, Anda perlu mengukur performa CPU dan GPU, dan menyeimbangkan pekerjaan antara dua jenis prosesor.

Dokumen ini tidak mencakup topik yang terkait dengan mengukur performa GPU atau menyeimbangkan pekerjaan antara CPU dan GPU. Jika Anda ingin lebih memahami performa GPU (atau kartu video tertentu), kunjungi situs web vendor untuk mencari informasi selengkapnya tentang performa GPU. Sebagai gantinya, dokumen ini berfokus pada pekerjaan yang dilakukan oleh runtime dan driver dengan mengurangi pekerjaan GPU ke jumlah yang dapat diabaikan. Ini, sebagian, berdasarkan pengalaman bahwa aplikasi yang mengalami masalah performa umumnya terbatas pada CPU.

Pengoptimalan Runtime dan Driver Dapat Menutupi Pengukuran API

Runtime memiliki pengoptimalan performa yang terpasang di dalamnya yang dapat membantah pengukuran panggilan individu. Berikut adalah contoh skenario yang menunjukkan masalah ini. Pertimbangkan urutan render berikut:

  BeginScene();
    ...
    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
    ...
  EndScene();
  Present();

Contoh 1: Urutan Render Sederhana

Melihat hasil untuk dua panggilan dalam urutan render, profiler instrumenting dapat mengembalikan hasil yang mirip dengan ini:

Number of cycles for SetTexture       : 100
Number of cycles for DrawPrimitive    : 950,500

Profiler mengembalikan jumlah siklus CPU yang diperlukan untuk memproses pekerjaan yang terkait dengan setiap panggilan (ingat bahwa GPU tidak disertakan dalam angka-angka ini karena GPU belum mulai mengerjakan perintah ini). Karena IDirect3DDevice9::D rawPrimitive membutuhkan hampir satu juta siklus untuk diproses, Anda dapat menyimpulkan bahwa itu tidak terlalu efisien. Namun, Anda akan segera melihat mengapa kesimpulan ini salah dan bagaimana Anda dapat menghasilkan hasil yang dapat digunakan untuk penganggaran.

Mengukur Perubahan Status Memerlukan Urutan Render yang Cermat

Semua panggilan selain IDirect3DDevice9::D rawPrimitive, DrawIndexedPrimitive, atau Clear (seperti SetTexture, SetVertexDeclaration, dan SetRenderState) menghasilkan perubahan status. Setiap perubahan status menetapkan status alur yang mengontrol bagaimana penyajian akan dilakukan.

Pengoptimalan dalam runtime dan/atau driver dirancang untuk mempercepat penyajian dengan mengurangi jumlah pekerjaan yang diperlukan. Berikut ini adalah beberapa pengoptimalan perubahan status yang dapat mencemari rata-rata profil:

  • Driver (atau runtime) dapat menyimpan perubahan status sebagai status lokal. Karena driver dapat beroperasi dalam algoritma "malas" (menunda pekerjaan sampai benar-benar diperlukan), pekerjaan yang terkait dengan beberapa perubahan status bisa tertunda.
  • Runtime (atau driver) dapat menghapus perubahan status dengan mengoptimalkan. Contohnya mungkin adalah menghapus perubahan status redundan yang menonaktifkan pencahayaan karena pencahayaan sebelumnya telah dinonaktifkan.

Tidak ada cara yang mudah untuk melihat urutan render dan menyimpulkan perubahan status mana yang akan mengatur sedikit kotor dan menunda pekerjaan, atau hanya akan dihapus dengan pengoptimalan. Bahkan jika Anda dapat mengidentifikasi perubahan status yang dioptimalkan dalam runtime atau driver hari ini, runtime atau driver besok kemungkinan akan diperbarui. Anda juga tidak mudah mengetahui apa status sebelumnya sehingga sulit untuk mengidentifikasi perubahan status redundan. Satu-satunya cara untuk memverifikasi biaya perubahan status adalah dengan mengukur urutan render yang menyertakan perubahan status.

Seperti yang Anda lihat, komplikasi yang disebabkan oleh memiliki beberapa prosesor, perintah yang diproses oleh lebih dari satu komponen, dan pengoptimalan yang dibangun ke dalam komponen membuat pembuatan profil sulit diprediksi. Di bagian berikutnya, masing-masing tantangan pembuatan profil ini akan diatasi. Sampel urutan render Direct3D akan ditampilkan, dengan teknik pengukuran yang menyertainya. Dengan pengetahuan ini, Anda akan dapat menghasilkan pengukuran yang akurat dan dapat diulang pada panggilan individu.

Cara Memprofilkan Urutan Render Direct3D secara Akurat

Sekarang setelah beberapa tantangan pembuatan profil telah disorot, bagian ini akan menunjukkan teknik yang akan membantu Anda menghasilkan pengukuran profil yang dapat digunakan untuk anggaran. Pengukuran pembuatan profil yang akurat dan dapat diulang dimungkinkan jika Anda memahami hubungan antara komponen yang dikontrol oleh CPU, dan cara menghindari pengoptimalan performa yang diterapkan oleh runtime dan driver.

Untuk memulai, Anda harus dapat mengukur waktu eksekusi satu panggilan API secara akurat.

Pilih Alat Pengukuran Yang Akurat Seperti QueryPerformanceCounter

Sistem operasi Microsoft Windows mencakup timer resolusi tinggi yang dapat digunakan untuk mengukur waktu yang berlalu resolusi tinggi. Nilai saat ini dari salah satu timer tersebut dapat dikembalikan menggunakan QueryPerformanceCounter. Setelah memanggil QueryPerformanceCounter untuk mengembalikan nilai mulai dan berhenti, perbedaan antara kedua nilai dapat dikonversi ke waktu yang berlalu aktual (dalam detik) menggunakan QueryPerformanceCounter.

Keuntungan menggunakan QueryPerformanceCounter adalah tersedia di Windows dan mudah digunakan. Cukup kelilingi panggilan dengan panggilan QueryPerformanceCounter dan simpan nilai mulai dan hentikan. Oleh karena itu, makalah ini akan menunjukkan cara menggunakan QueryPerformanceCounter untuk waktu eksekusi profil, mirip dengan cara profiler instrumenting akan mengukurnya. Berikut adalah contoh yang memperlihatkan cara menyematkan QueryPerformanceCounter di kode sumber Anda:

  BeginScene();
    ...
    // Start profiling
    LARGE_INTEGER start, stop, freq;
    QueryPerformanceCounter(&start);

    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); 

    QueryPerformanceCounter(&stop);
    stop.QuadPart -= start.QuadPart;
    QueryPerformanceFrequency(&freq);
    // Stop profiling
    ...
  EndScene();
  Present();

Contoh 2: Implementasi Pembuatan Profil Kustom dengan QPC

start and stop adalah dua bilangan bulat besar yang akan menahan nilai mulai dan berhenti yang dikembalikan oleh timer berkinerja tinggi. Perhatikan bahwa QueryPerformanceCounter(&start) dipanggil tepat sebelum SetTexture dan QueryPerformanceCounter(&stop) dipanggil tepat setelah DrawPrimitive. Setelah mendapatkan nilai stop, QueryPerformanceFrequency dipanggil untuk mengembalikan freq, yang merupakan frekuensi timer resolusi tinggi. Dalam contoh hipotetis ini, misalkan Anda mendapatkan hasil berikut untuk mulai, hentikan, dan freq:

Variabel Lokal Jumlah Tanda Centang
mulai 1792998845094
stop 1792998845102
Frek 3579545

 

Anda dapat mengonversi nilai-nilai ini ke jumlah siklus yang diperlukan untuk menjalankan panggilan API seperti ini:

# ticks = (stop - start) = 1792998845102 - 1792998845094 = 8 ticks

# cycles = CPU speed * number of ticks / QPF
# 4568   = 2 GHz      * 8              / 3,579,545

Dengan kata lain, dibutuhkan sekitar 4568 siklus jam untuk memproses SetTexture dan DrawPrimitive pada mesin 2 GHz ini. Anda dapat mengonversi nilai-nilai ini ke waktu aktual yang diperlukan untuk menjalankan semua panggilan seperti ini:

(stop - start)/ freq = elapsed time
8 ticks / 3,579,545 = 2.2E-6 seconds or between 2 and 3 microseconds.

Menggunakan QueryPerformanceCounter mengharuskan Anda menambahkan pengukuran mulai dan hentikan ke urutan render Anda dan menggunakan QueryPerformanceFrequency untuk mengonversi perbedaan (jumlah tanda centang) ke jumlah siklus CPU atau ke waktu aktual. Mengidentifikasi teknik pengukuran adalah awal yang baik untuk mengembangkan implementasi pembuatan profil kustom. Tetapi sebelum Anda melompat dan mulai membuat pengukuran, Anda perlu tahu cara menangani kartu video.

Fokus pada Pengukuran CPU

Seperti yang dinyatakan sebelumnya, CPU dan GPU bekerja secara paralel untuk memproses pekerjaan yang dihasilkan oleh panggilan API. Aplikasi dunia nyata mengharuskan pembuatan profil kedua jenis prosesor untuk mengetahui apakah aplikasi Anda terbatas pada CPU atau terbatas GPU. Karena performa GPU khusus vendor, akan sangat menantang untuk menghasilkan hasil dalam makalah ini yang mencakup berbagai kartu video yang tersedia.

Sebaliknya, makalah ini hanya akan berfokus pada pembuatan profil pekerjaan yang dilakukan oleh CPU dengan menggunakan teknik kustom untuk mengukur waktu proses dan pekerjaan driver. Pekerjaan GPU akan dikurangi menjadi jumlah yang tidak signifikan, sehingga hasil CPU lebih terlihat. Salah satu manfaat dari pendekatan ini adalah bahwa teknik ini menghasilkan lampiran yang harus dapat Anda korelasikan dengan pengukuran Anda. Untuk mengurangi pekerjaan yang diperlukan oleh kartu video ke tingkat yang tidak signifikan, cukup kurangi pekerjaan penyajian sekecil mungkin. Ini dapat dicapai dengan membatasi panggilan gambar untuk merender satu segitiga, dan dapat dibatasi lebih lanjut sehingga setiap segitiga hanya berisi satu piksel.

Unit pengukuran yang digunakan dalam makalah ini untuk mengukur pekerjaan CPU akan menjadi jumlah siklus jam CPU daripada waktu aktual. Siklus jam CPU memiliki keuntungan bahwa siklus jam tersebut lebih portabel (untuk aplikasi terbatas CPU) daripada waktu yang berlalu aktual di seluruh mesin dengan kecepatan CPU yang berbeda. Ini dapat dengan mudah dikonversi ke waktu aktual jika diinginkan.

Dokumen ini tidak mencakup topik yang terkait dengan menyeimbangkan beban kerja antara CPU dan GPU. Ingat, tujuan makalah ini bukan untuk mengukur performa keseluruhan aplikasi, tetapi untuk menunjukkan kepada Anda cara mengukur waktu yang diperlukan runtime dan driver untuk memproses panggilan API secara akurat. Dengan pengukuran yang akurat ini, Anda dapat mengambil tugas untuk menganggarkan CPU untuk memahami skenario performa tertentu.

Mengontrol Pengoptimalan Runtime dan Driver

Dengan teknik pengukuran yang diidentifikasi, dan strategi untuk mengurangi pekerjaan GPU, langkah selanjutnya adalah memahami pengoptimalan runtime dan driver yang menghalangi saat Anda membuat profil.

Pekerjaan CPU dapat dibagi menjadi tiga wadah: pekerjaan aplikasi, pekerjaan runtime, dan pekerjaan driver. Abaikan pekerjaan aplikasi karena ini berada di bawah kontrol programmer. Dari sudut triwulan aplikasi, runtime dan driver seperti kotak hitam, karena aplikasi tidak memiliki kontrol atas apa yang diterapkan di dalamnya. Kuncinya adalah memahami teknik pengoptimalan yang dapat diimplementasikan dalam runtime dan driver. Jika Anda tidak memahami pengoptimalan ini, sangat mudah untuk melompat ke kesimpulan yang salah tentang jumlah pekerjaan yang dilakukan CPU berdasarkan pengukuran profil. Secara khusus, ada dua topik yang terkait dengan sesuatu yang disebut buffer perintah dan apa yang dapat dilakukan untuk mengaburkan pembuatan profil. Topik-topik ini adalah:

  • Pengoptimalan runtime dengan Buffer Perintah. Buffer perintah adalah pengoptimalan runtime yang mengurangi dampak transisi mode. Untuk mengontrol waktu transisi mode, lihat Mengontrol Buffer Perintah.
  • Meniru efek waktu Buffer Perintah. Waktu transisi mode yang berlalu dapat berdampak besar pada pengukuran pembuatan profil. Strategi untuk ini adalah Membuat Urutan Render Besar Dibandingkan dengan Transisi Mode.

Mengontrol Buffer Perintah

Ketika aplikasi melakukan panggilan API, runtime mengonversi panggilan API ke format independen perangkat (yang akan kami panggil perintah), dan menyimpannya di buffer perintah. Buffer perintah ditambahkan ke diagram berikut.

diagram of cpu components, including a command buffer

Setiap kali aplikasi melakukan panggilan API lain, runtime mengulangi urutan ini dan menambahkan perintah lain ke buffer perintah. Pada titik tertentu, runtime mengikat buffer (mengirim perintah ke driver). Di Windows XP, mengosongkan buffer perintah menyebabkan transisi mode saat sistem operasi beralih dari runtime (berjalan dalam mode pengguna) ke driver (berjalan dalam mode kernel), seperti yang ditunjukkan pada diagram berikut.

  • mode pengguna - Mode prosesor non-istimewa yang menjalankan kode aplikasi. Aplikasi mode pengguna tidak dapat memperoleh akses ke data sistem kecuali melalui layanan sistem.
  • mode kernel - Mode prosesor istimewa tempat kode eksekutif berbasis Windows berjalan. Driver atau utas yang berjalan dalam mode kernel memiliki akses ke semua memori sistem, akses langsung ke perangkat keras, dan instruksi CPU untuk melakukan I/O dengan perangkat keras.

diagram of transitions between user mode and kernel mode

Transisi terjadi setiap kali CPU beralih dari pengguna ke mode kernel (dan sebaliknya) dan jumlah siklus yang diperlukan berukuran besar dibandingkan dengan panggilan API individual. Jika runtime mengirim setiap panggilan API ke driver saat dipanggil, setiap panggilan API akan dikenakan biaya transisi mode.

Sebaliknya, buffer perintah adalah pengoptimalan runtime yang dirancang untuk mengurangi biaya transisi mode yang efektif. Buffer perintah mengantrekan banyak perintah driver sebagai persiapan untuk transisi mode tunggal. Ketika runtime menambahkan perintah ke buffer perintah, kontrol dikembalikan ke aplikasi. Profiler tidak memiliki cara untuk mengetahui bahwa perintah driver mungkin bahkan belum dikirim ke driver. Akibatnya, angka yang dikembalikan oleh profiler instrumenting off-the-shelf menyesatkan karena mengukur pekerjaan runtime tetapi bukan pekerjaan driver terkait.

Hasil Profil tanpa Transisi Mode

Dengan menggunakan urutan render dari contoh 2, berikut adalah beberapa pengukuran waktu umum yang menggambarkan besarnya transisi mode. Dengan asumsi bahwa panggilan SetTexture dan DrawPrimitive tidak menyebabkan transisi mode, profiler instrumenting off-the-shelf dapat mengembalikan hasil yang mirip dengan ini:

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900

Masing-masing angka ini adalah jumlah waktu yang diperlukan runtime untuk menambahkan panggilan ini ke buffer perintah. Karena tidak ada transisi mode, driver belum melakukan pekerjaan apa pun. Hasil profiler akurat, tetapi mereka tidak mengukur semua pekerjaan yang urutan render akhirnya akan menyebabkan CPU dilakukan.

Hasil Profil dengan Transisi Mode

Sekarang, lihat apa yang terjadi untuk contoh yang sama ketika transisi mode terjadi. Kali ini, asumsikan SetTexture dan DrawPrimitive menyebabkan transisi mode. Sekali lagi, profiler instrumenting off-the-shelf dapat mengembalikan hasil yang mirip dengan ini:

Number of cycles for SetTexture           : 98 
Number of cycles for DrawPrimitive        : 946,900

Waktu yang diukur untuk SetTexture hampir sama, namun, peningkatan dramatis dalam jumlah waktu yang dihabiskan di DrawPrimitive disebabkan oleh transisi mode. Berikut adalah apa yang terjadi:

  1. Asumsikan buffer perintah memiliki ruang untuk satu perintah sebelum urutan render kami dimulai.
  2. SetTexture dikonversi ke format independen perangkat dan ditambahkan ke buffer perintah. Dalam skenario ini, panggilan ini mengisi buffer perintah.
  3. Runtime mencoba menambahkan DrawPrimitive ke buffer perintah tetapi tidak dapat, karena penuh. Sebagai gantinya, runtime mengikat buffer perintah. Hal ini menyebabkan transisi mode kernel. Asumsikan transisi membutuhkan sekitar 5000 siklus. Waktu ini berkontribusi pada waktu yang dihabiskan di DrawPrimitive.
  4. Driver kemudian memproses pekerjaan yang terkait dengan semua perintah yang dikosongkan dari buffer perintah. Asumsikan bahwa waktu driver untuk memproses perintah yang hampir mengisi buffer perintah adalah sekitar 935.000 siklus. Asumsikan bahwa pekerjaan driver yang terkait dengan SetTexture adalah sekitar 2750 siklus. Waktu ini berkontribusi pada waktu yang dihabiskan di DrawPrimitive.
  5. Ketika driver menyelesaikan pekerjaannya, transisi mode pengguna mengembalikan kontrol ke runtime. Buffer perintah sekarang kosong. Asumsikan transisi membutuhkan sekitar 5000 siklus.
  6. Urutan render selesai dengan mengonversi DrawPrimitive dan menambahkannya ke buffer perintah. Asumsikan ini membutuhkan sekitar 900 siklus. Waktu ini berkontribusi pada waktu yang dihabiskan di DrawPrimitive.

Meringkas hasilnya, Anda akan melihat:

DrawPrimitive = kernel-transition + driver work    + user-transition + runtime work
DrawPrimitive = 5000              + 935,000 + 2750 + 5000            + 900
DrawPrimitive = 947,950  

Sama seperti pengukuran untuk DrawPrimitive tanpa transisi mode (900 siklus), pengukuran untuk DrawPrimitive dengan transisi mode (947.950 siklus) akurat tetapi tidak berguna dalam hal penganggaran pekerjaan CPU. Hasilnya berisi pekerjaan runtime yang benar, pekerjaan driver untuk SetTexture, driver bekerja untuk perintah apa pun yang mendahului SetTexture, dan dua transisi mode. Namun, pengukuran kehilangan pekerjaan driver DrawPrimitive .

Transisi mode dapat terjadi sebagai respons terhadap panggilan apa pun. Ini tergantung pada apa yang sebelumnya ada di buffer perintah. Anda perlu mengontrol transisi mode untuk memahami berapa banyak pekerjaan CPU (runtime dan driver) yang terkait dengan setiap panggilan. Untuk melakukannya, Anda memerlukan mekanisme untuk mengontrol buffer perintah dan waktu transisi mode.

Mekanisme Kueri

Mekanisme kueri di Microsoft Direct3D 9 dirancang untuk memungkinkan runtime mengkueri GPU untuk kemajuan dan mengembalikan data tertentu dari GPU. Saat membuat profil, jika pekerjaan GPU diminimalkan sehingga memiliki dampak yang dapat diabaikan pada performa, Anda dapat mengembalikan status dari GPU untuk membantu mengukur pekerjaan driver. Lagipula, pekerjaan driver selesai ketika GPU telah melihat perintah driver. Selain itu, mekanisme kueri dapat dikoakkan untuk mengontrol dua karakteristik buffer perintah yang penting untuk pembuatan profil: ketika buffer perintah bermunculan dan berapa banyak pekerjaan dalam buffer.

Berikut adalah urutan render yang sama menggunakan mekanisme kueri:

// 1. Create an event query from the current device
IDirect3DQuery9* pEvent;
m_pD3DDevice->CreateQuery(D3DQUERYTYPE_EVENT, &pEvent);

// 2. Add an end marker to the command buffer queue.
pEvent->Issue(D3DISSUE_END);

// 3. Empty the command buffer and wait until the GPU is idle.
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;

// 4. Start profiling
LARGE_INTEGER start, stop;
QueryPerformanceCounter(&start);

// 5. Invoke the API calls to be profiled.
SetTexture(...);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

// 6. Add an end marker to the command buffer queue.
pEvent->Issue(D3DISSUE_END);

// 7. Force the driver to execute the commands from the command buffer.
// Empty the command buffer and wait until the GPU is idle.
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;
    
// 8. End profiling
QueryPerformanceCounter(&stop);

Contoh 3: Menggunakan Kueri untuk Mengontrol Buffer Perintah

Berikut adalah penjelasan yang lebih rinci tentang masing-masing baris kode ini:

  1. Buat kueri peristiwa dengan membuat objek kueri dengan D3DQUERYTYPE_EVENT.
  2. Tambahkan penanda peristiwa kueri ke buffer perintah dengan memanggil Masalah (D3DISSUE_END). Penanda ini memberi tahu driver untuk melacak ketika GPU selesai menjalankan perintah apa pun yang mendahului penanda.
  3. Panggilan pertama menginisiasi buffer perintah karena memanggil GetData dengan D3DGETDATA_FLUSH memaksa buffer perintah dikosongkan. Setiap panggilan berikutnya sedang memeriksa GPU untuk melihat kapan selesai memproses semua pekerjaan buffer perintah. Perulangan ini tidak mengembalikan S_OK hingga GPU diam.
  4. Contoh waktu mulai.
  5. Panggil panggilan API yang sedang diprofilkan.
  6. Tambahkan penanda peristiwa kueri kedua ke buffer perintah. Penanda ini akan digunakan untuk melacak penyelesaian panggilan.
  7. Panggilan pertama menginisiasi buffer perintah karena memanggil GetData dengan D3DGETDATA_FLUSH memaksa buffer perintah dikosongkan. Ketika GPU selesai memproses semua pekerjaan buffer perintah, GetData mengembalikan S_OK, dan perulangan keluar karena GPU menganggur.
  8. Sampel waktu berhenti.

Berikut adalah hasil yang diukur dengan QueryPerformanceCounter dan QueryPerformanceFrequency:

Variabel Lokal Jumlah Tanda Centang
mulai 1792998845060
stop 1792998845090
Frek 3579545

 

Mengonversi kutu menjadi siklus sekali lagi (pada mesin 2 GHz):

# ticks  = (stop - start) = 1792998845090 - 1792998845060 = 30 ticks
# cycles = CPU speed * number of ticks / QPF
# 16,450 = 2 GHz      * 30             / 3,579,545

Berikut adalah perincian jumlah siklus per panggilan:

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900
Number of cycles for Issue                : 200
Number of cycles for GetData              : 16,450

Mekanisme kueri telah memungkinkan kami mengontrol runtime dan pekerjaan driver yang sedang diukur. Untuk memahami masing-masing angka ini, berikut adalah apa yang terjadi sebagai respons terhadap setiap panggilan API, bersama dengan perkiraan waktu:

  1. Panggilan pertama menginisiasi buffer perintah dengan memanggil GetData dengan D3DGETDATA_FLUSH. Ketika GPU selesai memproses semua pekerjaan buffer perintah, GetData mengembalikan S_OK, dan perulangan keluar karena GPU menganggur.

  2. Urutan render dimulai dengan mengonversi SetTexture ke format independen perangkat dan menambahkannya ke buffer perintah. Asumsikan ini membutuhkan sekitar 100 siklus.

  3. DrawPrimitive dikonversi dan ditambahkan ke buffer perintah. Asumsikan ini membutuhkan sekitar 900 siklus.

  4. Masalah menambahkan penanda kueri ke buffer perintah. Asumsikan ini membutuhkan sekitar 200 siklus.

  5. GetData menyebabkan buffer perintah dikosongkan yang memaksa transisi mode kernel. Asumsikan ini membutuhkan sekitar 5000 siklus.

  6. Driver kemudian memproses pekerjaan yang terkait dengan keempat panggilan. Asumsikan bahwa waktu driver untuk memproses SetTexture adalah sekitar 2964 siklus, DrawPrimitive adalah sekitar 3600 siklus, Masalahnya sekitar 200 siklus. Jadi total waktu driver untuk keempat perintah adalah sekitar 6450 siklus.

    Catatan

    Driver juga membutuhkan sedikit waktu untuk melihat apa status GPU. Karena pekerjaan GPU sepele, GPU harus sudah dilakukan. GetData akan mengembalikan S_OK berdasarkan kemungkinan GPU selesai.

     

  7. Ketika driver menyelesaikan pekerjaannya, transisi mode pengguna mengembalikan kontrol ke runtime. Buffer perintah sekarang kosong. Asumsikan ini membutuhkan sekitar 5000 siklus.

Angka untuk GetData meliputi:

GetData = kernel-transition + driver work + user-transition
GetData = 5000              + 6450        + 5000           
GetData = 16,450  

driver work = SetTexture + DrawPrimitive + Issue = 
driver work = 2964       + 3600          + 200   = 6450 cycles 

Mekanisme kueri yang digunakan dalam kombinasi dengan QueryPerformanceCounter mengukur semua pekerjaan CPU. Ini dilakukan dengan kombinasi penanda kueri, dan perbandingan status kueri. Mulai dan hentikan penanda kueri yang ditambahkan ke buffer perintah digunakan untuk mengontrol berapa banyak pekerjaan dalam buffer. Dengan menunggu sampai kode pengembalian yang tepat dikembalikan, pengukuran mulai dilakukan tepat sebelum urutan render bersih dimulai, dan pengukuran berhenti dilakukan tepat setelah driver menyelesaikan pekerjaan yang terkait dengan konten buffer perintah. Ini secara efektif menangkap pekerjaan CPU yang dilakukan oleh runtime serta driver.

Sekarang setelah Anda tahu tentang buffer perintah dan efek yang dapat dimilikinya pada pembuatan profil, Anda harus tahu bahwa ada beberapa kondisi lain yang dapat menyebabkan runtime mengosongkan buffer perintah. Anda perlu memperhatikan ini dalam urutan render Anda. Beberapa kondisi ini sebagai respons terhadap panggilan API, yang lain menanggapi perubahan sumber daya dalam runtime. Salah satu kondisi berikut akan menyebabkan transisi mode:

  • Ketika salah satu metode kunci (Kunci) dipanggil pada buffer vertex, buffer indeks, atau tekstur (dalam kondisi tertentu dengan bendera tertentu).
  • Saat perangkat atau buffer vertex, buffer indeks, atau tekstur dibuat.
  • Saat perangkat atau buffer vertex, buffer indeks, atau tekstur dihancurkan oleh rilis terakhir.
  • Ketika ValidateDevice dipanggil.
  • Ketika Ada dipanggil.
  • Ketika buffer perintah terisi.
  • Ketika GetData dipanggil dengan D3DGETDATA_FLUSH.

Berhati-hatilah untuk memperhatikan kondisi ini dalam urutan render Anda. Setiap kali transisi mode ditambahkan, 10.000 siklus pekerjaan driver akan ditambahkan ke pengukuran pembuatan profil Anda. Selain itu, buffer perintah tidak berukuran statis. Runtime dapat mengubah ukuran buffer sebagai respons terhadap jumlah pekerjaan yang dihasilkan oleh aplikasi. Ini adalah pengoptimalan lain yang bergantung pada urutan render.

Jadi berhati-hatilah untuk mengontrol transisi mode selama pembuatan profil. Mekanisme kueri menawarkan metode yang kuat untuk mengosongkan buffer perintah sehingga Anda dapat mengontrol waktu transisi mode serta jumlah pekerjaan yang dikandung buffer. Namun, bahkan teknik ini dapat ditingkatkan dengan mengurangi waktu transisi mode untuk membuatnya tidak signifikan sehubungan dengan hasil yang diukur.

Buat Urutan Render Besar Dibandingkan dengan Transisi Mode

Dalam contoh sebelumnya, sakelar mode kernel dan sakelar mode pengguna menggunakan sekitar 10.000 siklus yang tidak ada hubungannya dengan runtime dan pekerjaan driver. Karena transisi mode dibangun ke dalam sistem operasi, transisi tidak dapat dikurangi menjadi nol. Untuk membuat transisi mode tidak signifikan, urutan render perlu disesuaikan sehingga driver dan pekerjaan runtime adalah urutan yang lebih besar dari sakelar mode. Anda dapat mencoba melakukan pengurangan untuk menghapus transisi, tetapi mengamortisasi biaya atas biaya urutan render yang jauh lebih besar lebih dapat diandalkan.

Strategi untuk mengurangi transisi mode sampai menjadi tidak signifikan adalah menambahkan perulangan ke urutan render. Misalnya, mari kita lihat hasil pembuatan profil jika perulangan ditambahkan yang akan mengulangi urutan render 1500 kali:

// Initialize the array with two textures, same size, same format
IDirect3DTexture* texArray[2];

CreateQuery(D3DQUERYTYPE_EVENT, pEvent);
pEvent->Issue(D3DISSUE_END);
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;

LARGE_INTEGER start, stop;
// Now start counting because the video card is ready
QueryPerformanceCounter(&start);

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  SetTexture(taxArray[i%2]);
  DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
}

pEvent->Issue(D3DISSUE_END);

while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;
QueryPerformanceCounter(&stop);

Contoh 4: Tambahkan Perulangan ke Urutan Render

Berikut adalah hasil yang diukur dengan QueryPerformanceCounter dan QueryPerformanceFrequency:

Variabel Lokal Jumlah Tics
mulai 1792998845000
stop 1792998847084
Frek 3579545

 

Menggunakan QueryPerformanceCounter mengukur 2.840 tick sekarang. Mengonversi kutu ke siklus sama seperti yang telah kami tunjukkan:

# ticks  = (stop - start) = 1792998847084 - 1792998845000 = 2840 ticks
# cycles    = machine speed * number of ticks / QPF
# 6,900,000 = 2 GHz          * 2840           / 3,579,545

Dengan kata lain, dibutuhkan sekitar 6,9 juta siklus pada mesin 2 GHz ini untuk memproses 1500 panggilan dalam perulangan render. Dari 6,9 juta siklus, jumlah waktu dalam transisi mode sekitar 10k, jadi sekarang hasil profil hampir sepenuhnya mengukur pekerjaan yang terkait dengan SetTexture dan DrawPrimitive.

Perhatikan bahwa sampel kode memerlukan array dua tekstur. Untuk menghindari pengoptimalan runtime yang akan menghapus SetTexture jika mengatur penunjuk tekstur yang sama setiap kali dipanggil, cukup gunakan array dua tekstur. Dengan begitu, setiap kali melalui perulangan, penunjuk tekstur berubah, dan pekerjaan penuh yang terkait dengan SetTexture dilakukan. Pastikan kedua tekstur memiliki ukuran dan format yang sama, sehingga tidak ada status lain yang akan berubah saat teksturnya.

Dan sekarang Anda memiliki teknik untuk membuat profil Direct3D. Ini bergantung pada penghitung kinerja tinggi (QueryPerformanceCounter) untuk merekam jumlah tick yang diperlukan CPU untuk memproses pekerjaan. Pekerjaan dikontrol dengan hati-hati agar menjadi runtime dan pekerjaan driver yang terkait dengan panggilan API menggunakan mekanisme kueri. Kueri menyediakan dua sarana kontrol: pertama untuk mengosongkan buffer perintah sebelum urutan render dimulai, dan kedua untuk kembali ketika pekerjaan GPU selesai.

Sejauh ini, makalah ini telah menunjukkan cara membuat profil urutan render. Setiap urutan render telah cukup sederhana, berisi satu panggilan DrawPrimitive dan panggilan SetTexture. Ini dilakukan untuk fokus pada buffer perintah dan penggunaan mekanisme kueri untuk mengontrolnya. Berikut adalah ringkasan singkat tentang cara membuat profil urutan render arbitrer:

  • Gunakan penghitung performa tinggi seperti QueryPerformanceCounter untuk mengukur waktu yang diperlukan untuk memproses setiap panggilan API. Gunakan QueryPerformanceFrequency dan laju jam CPU untuk mengonversinya ke jumlah siklus CPU per panggilan API.
  • Minimalkan jumlah pekerjaan GPU dengan merender daftar segitiga, di mana setiap segitiga berisi satu piksel.
  • Gunakan mekanisme kueri untuk mengosongkan buffer perintah sebelum urutan render. Ini menjamin bahwa pembuatan profil akan menangkap jumlah runtime dan pekerjaan driver yang benar yang terkait dengan urutan render.
  • Kontrol jumlah pekerjaan yang ditambahkan ke buffer perintah dengan penanda peristiwa kueri. Kueri yang sama ini mendeteksi kapan GPU menyelesaikan pekerjaannya. Karena pekerjaan GPU sepele, ini hampir setara dengan mengukur ketika pekerjaan driver selesai.

Semua teknik ini digunakan untuk memprofilkan perubahan status. Dengan asumsi bahwa Anda telah membaca dan memahami cara mengontrol buffer perintah, dan telah berhasil menyelesaikan pengukuran garis besar pada DrawPrimitive, Anda siap untuk menambahkan perubahan status ke urutan render Anda. Ada beberapa tantangan pembuatan profil tambahan saat menambahkan perubahan status ke urutan render. Jika Anda ingin menambahkan perubahan status ke urutan render Anda, pastikan untuk melanjutkan ke bagian berikutnya.

Membuat Profil Perubahan Status Direct3D

Direct3D menggunakan banyak status render untuk mengontrol hampir setiap aspek alur. API yang menyebabkan perubahan status mencakup fungsi atau metode apa pun selain panggilan Gambar*Primitif.

Perubahan status rumit karena Anda mungkin tidak dapat melihat biaya perubahan status tanpa penyajian. Ini adalah hasil dari algoritma malas yang digunakan driver dan GPU untuk menunda pekerjaan sampai benar-benar harus dilakukan. Secara umum, Anda harus mengikuti langkah-langkah ini untuk mengukur perubahan status tunggal:

  1. Profil DrawPrimitive terlebih dahulu.
  2. Tambahkan satu perubahan status ke urutan render dan profil urutan baru.
  3. Kurangi perbedaan antara kedua urutan untuk mendapatkan biaya perubahan status.

Secara alami, semua yang telah Anda pelajari tentang menggunakan mekanisme kueri dan menempatkan urutan render dalam perulangan untuk meniadakan biaya transisi mode yang masih berlaku.

Membuat Profil Perubahan Status Sederhana

Dimulai dengan urutan render yang berisi DrawPrimitive, berikut adalah urutan kode untuk mengukur biaya penambahan SetTexture:

// Get the start counter value as shown in Example 4 

// Initialize a texture array as shown in Example 4
IDirect3DTexture* texArray[2];

// Render sequence loop 
for(int i = 0; i < 1500; i++)
{
  SetTexture(0, texArray[i%2];
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
}

// Get the stop counter value as shown in Example 4 

Contoh 5: Mengukur Satu Panggilan API Perubahan Status

Perhatikan bahwa perulangan berisi dua panggilan, SetTexture dan DrawPrimitive. Urutan render mengulang 1500 kali dan menghasilkan hasil yang mirip dengan ini:

Variabel Lokal Jumlah Tics
mulai 1792998860000
stop 1792998870260
Frek 3579545

 

Mengonversi tick menjadi siklus sekali lagi menghasilkan:

# ticks  = (stop - start) = 1792998870260 - 1792998860000 = 10,260 ticks
# cycles    = machine speed * number of ticks / QPF
5,775,000   = 2 GHz          * 10,260         / 3,579,545

Pembagian dengan jumlah iterasi dalam hasil perulangan:

5,775,000 cycles / 1500 iterations = 3850 cycles for one iteration

Setiap perulangan perulangan berisi perubahan status dan panggilan gambar. Mengurangi hasil dari urutan render DrawPrimitive daun:

3850 - 1100 = 2750 cycles for SetTexture

Ini adalah jumlah rata-rata siklus untuk menambahkan SetTexture ke urutan render ini. Teknik yang sama ini dapat diterapkan pada perubahan status lainnya.

Mengapa SetTexture disebut perubahan status sederhana? Karena status yang sedang ditetapkan dibatasi sehingga alur melakukan jumlah pekerjaan yang sama setiap kali status diubah. Membatasi kedua tekstur dengan ukuran dan format yang sama menjamin jumlah pekerjaan yang sama untuk setiap panggilan SetTexture .

Membuat Profil Perubahan Status yang Perlu Diubah

Ada perubahan status lain yang menyebabkan jumlah pekerjaan yang dilakukan oleh alur grafis berubah untuk setiap perulangan perulangan render. Misalnya, jika pengujian z diaktifkan, setiap warna piksel memperbarui target render hanya setelah nilai z piksel baru diuji terhadap nilai z untuk piksel yang ada. Jika pengujian z dinonaktifkan, pengujian per piksel ini tidak dilakukan dan output ditulis jauh lebih cepat. Mengaktifkan atau menonaktifkan status z-test secara dramatis mengubah jumlah pekerjaan yang dilakukan (oleh CPU serta GPU) selama penyajian.

SetRenderState memerlukan status render tertentu dan nilai status untuk mengaktifkan atau menonaktifkan pengujian z. Nilai status tertentu dievaluasi pada runtime untuk menentukan berapa banyak pekerjaan yang diperlukan. Sulit untuk mengukur perubahan status ini dalam perulangan render dan masih melakukan prasyarat status alur sehingga beralih. Satu-satunya solusi adalah mengalihkan perubahan status selama urutan render.

Misalnya, teknik pembuatan profil perlu diulang dua kali sebagai berikut:

  1. Mulailah dengan membuat profil urutan render DrawPrimitive. Panggil ini garis besar.
  2. Buat profil urutan render kedua yang mengalihkan perubahan status. Perulangan urutan render berisi:
    • Perubahan status untuk mengatur status menjadi kondisi "false".
    • DrawPrimitive sama seperti urutan aslinya.
    • Perubahan status untuk mengatur status menjadi kondisi "true".
    • DrawPrimitive kedua untuk memaksa perubahan status kedua direalisasikan.
  3. Temukan perbedaan antara dua urutan render. Hal ini dilakukan dengan:

Dengan teknik perulangan yang digunakan dalam urutan render, biaya mengubah status alur perlu diukur dengan mengubah status dari kondisi "true" ke kondisi "false" dan sebaliknya, untuk setiap iterasi dalam urutan render. Arti "true" dan "false" di sini tidak harfiah, ini berarti bahwa status perlu diatur ke dalam kondisi yang berlawanan. Hal ini menyebabkan kedua perubahan status diukur selama pembuatan profil. Tentu saja semua yang telah Anda pelajari tentang menggunakan mekanisme kueri dan menempatkan urutan render dalam perulangan untuk meniadakan biaya transisi mode yang masih berlaku.

Misalnya, berikut adalah urutan kode untuk mengukur biaya pengalihan pengujian z pada atau nonaktif:

// Get the start counter value as shown in Example 4 

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  // Precondition the pipeline state to the "false" condition
  SetRenderState(D3DRS_ZENABLE, FALSE);
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1);

  // Set the pipeline state to the "true" condition
  SetRenderState(D3DRS_ZENABLE, TRUE);

  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

// Get the stop counter value as shown in Example 4 

Contoh 5: Mengukur Perubahan Status Pengalihan

Perulangan mengalihkan status dengan menjalankan dua panggilan SetRenderState. Panggilan SetRenderState pertama menonaktifkan pengujian z dan SetRenderState kedua memungkinkan pengujian z. Setiap SetRenderState diikuti oleh DrawPrimitive sehingga pekerjaan yang terkait dengan perubahan status diproses oleh driver alih-alih hanya mengatur bit kotor di driver.

Angka-angka ini wajar untuk urutan render ini:

Variabel Lokal Jumlah Tanda Centang
mulai 1792998845000
stop 1792998861740
Frek 3579545

 

Mengonversi tick menjadi siklus sekali lagi menghasilkan:

# ticks  = (stop - start) = 1792998861740 - 1792998845000 = 15,120 ticks
# cycles    = machine speed * number of ticks / QPF
 9,300,000  = 2 GHz          * 16,740         / 3,579,545

Pembagian dengan jumlah iterasi dalam hasil perulangan:

9,300,000 cycles / 1500 iterations = 6200 cycles for one iteration

Setiap perulangan perulangan berisi dua perubahan status dan dua panggilan gambar. Mengurangi panggilan gambar (dengan asumsi 1100 siklus) meninggalkan:

6200 - 1100 - 1100 = 4000 cycles for both state changes

Ini adalah jumlah rata-rata siklus untuk kedua perubahan status sehingga waktu rata-rata untuk setiap perubahan status adalah:

4000 / 2  = 2000 cycles for each state change

Oleh karena itu, jumlah rata-rata siklus untuk mengaktifkan atau menonaktifkan pengujian z adalah 2000 siklus. Perlu dicatat bahwa QueryPerformanceCounter mengukur z-enable setengah waktu dan z-disable setengah dari waktu. Teknik ini sebenarnya mengukur rata-rata kedua perubahan status. Dengan kata lain, Anda mengukur waktu untuk mengalihkan status. Dengan menggunakan teknik ini, Anda tidak memiliki cara untuk mengetahui apakah waktu aktifkan dan nonaktifkan setara karena Anda telah mengukur rata-rata keduanya. Namun demikian, ini adalah angka yang wajar untuk digunakan saat menganggarkan status pengalihan sebagai aplikasi yang menyebabkan perubahan status ini hanya dapat melakukannya dengan beralih status ini.

Jadi sekarang Anda dapat menerapkan teknik ini dan membuat profil semua perubahan status yang Anda inginkan, kan? Tidak cocok. Anda masih perlu berhati-hati tentang pengoptimalan yang dirancang untuk mengurangi jumlah pekerjaan yang perlu dilakukan. Ada dua jenis pengoptimalan yang harus Anda ketahui saat merancang urutan render Anda.

Perhatikan Pengoptimalan Perubahan Status

Bagian sebelumnya menunjukkan cara memprofilkan kedua jenis perubahan status: perubahan status sederhana yang dibatasi untuk menghasilkan jumlah pekerjaan yang sama untuk setiap perulangan, dan perubahan status beralih yang secara dramatis mengubah jumlah pekerjaan yang dilakukan. Apa yang terjadi jika Anda mengambil urutan render sebelumnya dan menambahkan perubahan status lain ke dalamnya? Misalnya, contoh ini mengambil urutan render z-enable> dan menambahkan perbandingan z-func ke dalamnya:

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  // Precondition the pipeline state to the opposite condition
  SetRenderState(D3DRS_ZFUNC, D3DCMP_NEVER);

  // Precondition the pipeline state to the opposite condition
  SetRenderState(D3DRS_ZENABLE, FALSE);
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1);

  // Now set the state change you want to measure
  SetRenderState(D3DRS_ZFUNC, D3DCMP_ALWAYS);

  // Now set the state change you want to measure
  SetRenderState(D3DRS_ZENABLE, TRUE);

  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

Status z-func mengatur tingkat perbandingan saat menulis ke z-buffer (antara nilai z piksel saat ini dengan nilai z piksel di buffer kedalaman). D3DCMP_NEVER menonaktifkan perbandingan pengujian z saat D3DCMP_ALWAYS mengatur perbandingan yang akan terjadi setiap kali pengujian z dilakukan.

Pembuatan profil salah satu perubahan status ini dalam urutan render dengan DrawPrimitive menghasilkan hasil yang mirip dengan ini:

Perubahan Status Tunggal Jumlah Rata-rata Siklus
D3DRS_ZENABLE saja 2000

 

or

Perubahan Status Tunggal Jumlah Rata-rata Siklus
D3DRS_ZFUNC saja 600

 

Tetapi, jika Anda memprofilkan D3DRS_ZENABLE dan D3DRS_ZFUNC dalam urutan render yang sama, Anda dapat melihat hasil seperti ini:

Kedua Perubahan Status Jumlah Rata-rata Siklus
D3DRS_ZENABLE + D3DRS_ZFUNC 2000

 

Anda dapat mengharapkan hasilnya adalah jumlah siklus 2000 dan 600 (atau 2600) karena driver melakukan semua pekerjaan yang terkait dengan pengaturan kedua status render. Sebaliknya, rata-rata adalah 2000 siklus.

Hasil ini mencerminkan pengoptimalan perubahan status yang diterapkan dalam runtime, driver, atau GPU. Dalam hal ini, driver dapat melihat SetRenderState pertama dan mengatur status kotor yang akan menunda pekerjaan sampai nanti. Ketika driver melihat SetRenderState kedua, status kotor yang sama dapat diatur secara berlebihan dan pekerjaan yang sama akan ditunda sekali lagi. Ketika DrawPrimitive dipanggil, pekerjaan yang terkait dengan status kotor akhirnya diproses. Driver menjalankan pekerjaan satu kali, yang berarti bahwa dua perubahan status pertama secara efektif dikonsolidasikan oleh driver. Demikian pula, perubahan status ketiga dan keempat secara efektif dikonsolidasikan oleh driver menjadi perubahan status tunggal ketika DrawPrimitive kedua dipanggil. Hasil bersihnya adalah bahwa driver dan GPU memproses perubahan status tunggal untuk setiap panggilan gambar.

Ini adalah contoh yang baik dari pengoptimalan driver yang bergantung pada urutan. Driver menunda pekerjaan dua kali dengan menetapkan keadaan kotor, dan kemudian melakukan pekerjaan sekali untuk menghapus keadaan kotor. Ini adalah contoh yang baik dari jenis peningkatan efisiensi yang dapat terjadi ketika pekerjaan ditangguhkan sampai benar-benar diperlukan.

Bagaimana Anda tahu perubahan status mana yang menetapkan status kotor secara internal dan oleh karena itu menunda pekerjaan sampai nanti? Hanya dengan menguji urutan render (atau berbicara dengan penulis driver). Driver diperbarui dan ditingkatkan secara berkala sehingga daftar pengoptimalan tidak statis. Hanya ada satu cara untuk benar-benar mengetahui biaya perubahan status dalam urutan render tertentu, pada sekumpulan perangkat keras tertentu; Dan itu adalah untuk mengukurnya.

Perhatikan Pengoptimalan DrawPrimitive

Selain pengoptimalan perubahan status, runtime akan mencoba mengoptimalkan jumlah panggilan gambar yang harus diproses driver. Misalnya, pertimbangkan kembali ini untuk menarik kembali panggilan:

DrawPrimitive(D3DPT_TRIANGLELIST, 0, 3); // Draw 3 primitives, vertices 0 - 8
DrawPrimitive(D3DPT_TRIANGLELIST, 9, 4); // Draw 4 primitives, vertices 9 - 20

Contoh 5a: Dua Panggilan Gambar

Urutan ini berisi dua panggilan gambar, yang runtime akan dikonsolidasikan ke dalam satu panggilan yang setara dengan:

DrawPrimitive(D3DPT_TRIANGLELIST, 0, 7); // Draw 7 primitives, vertices 0 - 20

Contoh 5b: Satu Panggilan Gambar Yang Digabungkan

Runtime akan menggabungkan kedua panggilan gambar khusus ini ke dalam satu panggilan, yang mengurangi pekerjaan driver sebesar 50 persen karena driver sekarang hanya perlu memproses satu panggilan gambar.

Secara umum, runtime akan menggabungkan dua atau beberapa panggilan DrawPrimitive back-to-back saat:

  1. Jenis primitif adalah daftar segitiga (D3DPT_TRIANGLELIST).
  2. Setiap panggilan DrawPrimitive berturut-turut harus mereferensikan simpul berturut-turut dalam buffer vertex.

Demikian pula, kondisi yang tepat untuk menggabungkan dua atau beberapa panggilan DrawIndexedPrimitive back-to-back adalah:

  1. Jenis primitif adalah daftar segitiga (D3DPT_TRIANGLELIST).
  2. Setiap panggilan DrawIndexedPrimitive berurutan harus mereferensikan indeks berurutan dalam buffer indeks.
  3. Setiap panggilan DrawIndexedPrimitive berturut-turut harus menggunakan nilai yang sama untuk BaseVertexIndex.

Untuk mencegah perangkaian selama pembuatan profil, ubah urutan render sehingga jenis primitif bukan daftar segitiga, atau ubah urutan render sehingga tidak ada panggilan gambar back-to-back yang menggunakan simpul berturut-turut (atau indeks). Lebih khusus lagi, runtime juga akan menggabungkan panggilan gambar yang memenuhi kedua kondisi berikut:

  • Saat panggilan sebelumnya adalah DrawPrimitive, jika panggilan gambar berikutnya:
    • menggunakan daftar segitiga, AND
    • menentukan StartVertex = StartVertex sebelumnya + PrimitiveCount sebelumnya * 3
  • Saat menggunakan DrawIndexedPrimitive, jika panggilan gambar berikutnya:
    • menggunakan daftar segitiga, AND
    • menentukan StartIndex = StartIndex sebelumnya + PrimitiveCount sebelumnya * 3, DAN
    • menentukan BaseVertexIndex = BaseVertexIndex sebelumnya

Berikut adalah contoh yang lebih halus dari penggalian panggilan gambar yang mudah diabaikan ketika Anda membuat profil. Asumsikan urutan render terlihat seperti ini:

  for(int i = 0; i < 1500; i++)
  {
    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
  }

Contoh 5c: Satu Perubahan Status dan Satu Panggilan Gambar

Perulangan berulang hingga 1500 segitiga, mengatur tekstur dan menggambar setiap segitiga. Perulangan render ini membutuhkan sekitar 2750 siklus untuk SetTexture dan 1100 siklus untuk DrawPrimitive seperti yang ditunjukkan di bagian sebelumnya. Anda mungkin secara intuitif mengharapkan bahwa memindahkan SetTexture di luar perulangan render harus mengurangi jumlah pekerjaan yang dilakukan oleh driver sebesar 1500 * 2750 siklus, yang merupakan jumlah pekerjaan yang terkait dengan panggilan SetTexture 1500 kali. Cuplikan kode akan terlihat seperti ini:

  SetTexture(...); // Set the state outside the loop
  for(int i = 0; i < 1500; i++)
  {
//    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
  }

Contoh 5d: Contoh 5c dengan Perubahan Status Di Luar Perulangan

Memindahkan SetTexture di luar perulangan render memang mengurangi jumlah pekerjaan yang terkait dengan SetTexture karena dipanggil sekali, bukan 1500 kali. Efek sekunder yang kurang jelas adalah bahwa pekerjaan untuk DrawPrimitive juga berkurang dari 1500 panggilan menjadi 1 panggilan karena semua kondisi untuk menggabungkan panggilan undian terpenuhi. Ketika urutan render diproses, runtime akan memproses 1500 panggilan ke dalam satu panggilan driver. Dengan memindahkan satu baris kode ini, jumlah pekerjaan driver telah berkurang secara dramatis:

total work done = runtime + driver work

Example 5c: with SetTexture in the loop:
runtime work = 1500 SetTextures + 1500 DrawPrimitives 
driver  work = 1500 SetTextures + 1500 DrawPrimitives 

Example 5d: with SetTexture outside of the loop:
runtime work = 1 SetTexture + 1 DrawPrimitive + 1499 Concatenated DrawPrimitives 
driver  work = 1 SetTexture + 1 DrawPrimitive 

Hasil ini sepenuhnya benar, tetapi sangat menyesatkan dalam konteks pertanyaan asli. Pengoptimalan panggilan gambar telah menyebabkan jumlah pekerjaan driver berkurang secara dramatis. Ini adalah masalah umum saat melakukan pembuatan profil kustom. Saat menghilangkan panggilan dari urutan render, berhati-hatilah untuk menghindari penggalian panggilan. Bahkan, skenario ini adalah contoh yang kuat dari jumlah peningkatan performa driver yang mungkin dilakukan oleh pengoptimalan runtime ini.

Jadi sekarang Anda tahu cara mengukur perubahan status. Mulailah dengan membuat profil DrawPrimitive. Kemudian tambahkan setiap perubahan status tambahan ke urutan (dalam beberapa kasus menambahkan satu panggilan dan dalam kasus lain menambahkan dua panggilan) dan mengukur perbedaan antara dua urutan. Anda dapat mengonversi hasilnya menjadi tanda centang atau siklus atau waktu. Sama seperti mengukur urutan render dengan QueryPerformanceCounter, mengukur perubahan status individual bergantung pada mekanisme kueri untuk mengontrol buffer perintah, dan menempatkan perubahan status dalam perulangan untuk meminimalkan dampak transisi mode. Teknik ini mengukur biaya pengalihan status, karena profiler mengembalikan rata-rata mengaktifkan dan menonaktifkan status.

Dengan kemampuan ini, Anda dapat mulai menghasilkan urutan penyajian arbitrer dan secara akurat mengukur runtime dan pekerjaan driver terkait. Angka-angka tersebut kemudian dapat digunakan untuk menjawab pertanyaan anggaran seperti "berapa banyak lagi panggilan ini" yang dapat dilakukan dalam urutan render sambil tetap mempertahankan kecepatan bingkai yang wajar, dengan asumsi skenario terbatas CPU.

Ringkasan

Makalah ini menunjukkan cara mengontrol buffer perintah sehingga panggilan individu dapat dibuat profilnya secara akurat. Angka pembuatan profil dapat dihasilkan dalam tanda centang, siklus, atau waktu absolut. Mereka mewakili jumlah runtime dan pekerjaan driver yang terkait dengan setiap panggilan API.

Mulailah dengan membuat profil panggilan Draw*Primitif dalam urutan render. Ingatlah untuk:

  1. Gunakan QueryPerformanceCounter untuk mengukur jumlah tanda centang per panggilan API. Gunakan QueryPerformanceFrequency untuk mengonversi hasil menjadi siklus atau waktu jika Anda mau.
  2. Gunakan mekanisme kueri untuk mengosongkan buffer perintah sebelum memulai.
  3. Sertakan urutan render dalam perulangan untuk meminimalkan dampak transisi mode.
  4. Gunakan mekanisme kueri untuk mengukur kapan GPU telah menyelesaikan pekerjaannya.
  5. Perhatikan perangkaian runtime yang akan berdampak besar pada jumlah pekerjaan yang dilakukan.

Ini memberi Anda performa dasar untuk DrawPrimitive yang dapat digunakan untuk membangun. Untuk membuat profil satu perubahan status, ikuti tips tambahan berikut:

  1. Tambahkan perubahan status ke profil urutan render yang diketahui urutan baru. Karena pengujian dilakukan dalam perulangan, ini memerlukan pengaturan status dua kali menjadi nilai yang berlawanan (seperti mengaktifkan dan menonaktifkan misalnya).
  2. Bandingkan perbedaan waktu siklus antara kedua urutan.
  3. Untuk perubahan status yang secara signifikan mengubah alur (seperti SetTexture), kurangi perbedaan antara dua urutan untuk mendapatkan waktu untuk perubahan status.
  4. Untuk perubahan status yang secara signifikan mengubah alur (dan karenanya memerlukan status pengalihan seperti SetRenderState), kurangi perbedaan antara urutan render dan bagi dengan 2. Ini akan menghasilkan jumlah rata-rata siklus untuk setiap perubahan status.

Tetapi berhati-hatilah terhadap pengoptimalan yang menyebabkan hasil yang tidak terduga saat pembuatan profil. Pengoptimalan perubahan status dapat menetapkan status kotor yang menyebabkan pekerjaan ditangguhkan. Ini dapat menyebabkan hasil profil yang tidak intuitif seperti yang diharapkan. Menarik panggilan yang digabungkan akan secara dramatis mengurangi pekerjaan driver yang dapat menyebabkan kesimpulan yang menyesatkan. Urutan render yang direncanakan dengan hati-hati digunakan untuk mencegah perubahan status dan menarik perangkaian panggilan agar tidak terjadi. Caranya adalah mencegah pengoptimalan terjadi selama pembuatan profil sehingga angka yang Anda hasilkan adalah angka anggaran yang wajar.

Catatan

Menduplikasi strategi pembuatan profil ini dalam aplikasi tanpa mekanisme kueri lebih sulit. Sebelum Direct3D 9, satu-satunya cara yang dapat diprediksi untuk mengosongkan buffer perintah adalah dengan mengunci permukaan aktif (seperti target render) untuk menunggu hingga GPU diam. Ini karena mengunci permukaan memaksa runtime untuk mengosongkan buffer perintah jika ada perintah penyajian di buffer yang harus memperbarui permukaan sebelum terkunci, selain menunggu GPU selesai. Teknik ini fungsional, meskipun lebih mengganggu bahwa menggunakan mekanisme kueri yang diperkenalkan dalam Direct3D 9.

 

Lampiran

Angka dalam tabel ini adalah rentang perkiraan untuk jumlah runtime dan pekerjaan driver yang terkait dengan masing-masing perubahan status ini. Perkiraan didasarkan pada pengukuran aktual yang dilakukan pada driver menggunakan teknik yang ditunjukkan dalam kertas. Angka-angka ini dihasilkan menggunakan runtime Direct3D 9 dan bergantung pada driver.

Teknik dalam makalah ini dirancang untuk mengukur runtime dan pekerjaan driver. Secara umum, tidak praktis untuk memberikan hasil yang cocok dengan performa CPU dan GPU di setiap aplikasi karena ini akan memerlukan array urutan render yang lengkap. Selain itu, sangat sulit untuk tolok ukur performa GPU karena sangat tergantung pada pengaturan status dalam alur sebelum urutan render. Misalnya, mengaktifkan pencampukan alfa tidak memengaruhi jumlah pekerjaan CPU yang diperlukan, tetapi dapat berdampak besar pada jumlah pekerjaan yang dilakukan oleh GPU. Oleh karena itu, teknik dalam makalah ini membatasi pekerjaan GPU hingga jumlah minimum yang mungkin dengan membatasi jumlah data yang perlu dirender. Ini berarti bahwa angka dalam tabel akan paling cocok dengan hasil yang dicapai dari aplikasi yang dibatasi CPU (dibandingkan dengan aplikasi yang dibatasi oleh GPU).

Anda dianjurkan untuk menggunakan teknik yang disajikan untuk mencakup skenario dan konfigurasi yang paling penting bagi Anda. Nilai dalam tabel dapat digunakan untuk membandingkan dengan angka yang Anda hasilkan. Karena setiap driver bervariasi, satu-satunya cara untuk menghasilkan angka aktual yang akan Anda lihat adalah dengan menghasilkan hasil pembuatan profil menggunakan skenario Anda.

Panggilan API Jumlah rata-rata Siklus
SetVertexDeclaration 6500 - 11250
SetFVF 6400 - 11200
SetVertexShader 3000 - 12100
SetPixelShader 6300 - 7000
SPEKULARENABLE 1900 - 11200
SetRenderTarget 6000 - 6250
SetPixelShaderConstant (1 Konstanta) 1500 - 9000
NORMALIZENORMALS 2200 - 8100
Dapat Dicerahkan 1300 - 9000
SetStreamSource 3700 - 5800
PENCAHAYAAN 1700 - 7500
DIFFUSEMATERIALSOURCE 900 - 8300
AMBIENTMATERIALSOURCE 900 - 8200
COLORVERTEX 800 - 7800
SetLight 2200 - 5100
SetTransform 3200 - 3750
SetIndices 900 - 5600
AMBIENT 1150 - 4800
SetTexture 2500 - 3100
SPECULARMATERIALSOURCE 900 - 4600
EMISSIVEMATERIALSOURCE 900 - 4500
SetMaterial 1000 - 3700
DAPAT DI-ZENABLE 700 - 3900
WRAP0 1600 - 2700
MINFILTER 1700 - 2500
MAGFILTER 1700 - 2400
SetVertexShaderConstant (1 Konstanta) 1000 - 2700
COLOROP 1500 - 2100
COLORARG2 1300 - 2000
COLORARG1 1300 - 1980
CULLMODE 500 - 2570
KLIPING 500 - 2550
DrawIndexedPrimitive 1200 - 1400
ADDRESSV 1090 - 1500
ADDRESSU 1070 - 1500
DrawPrimitive 1050 - 1150
SRGBTEXTURE 150 - 1500
STENCILMASK 570 - 700
STENCILZFAIL 500 - 800
STENCILREF 550 - 700
DAPAT DIDENDA ALFABEL 550 - 700
STENCILFUNC 560 - 680
STENCILWRITEMASK 520 - 700
STENCILFAIL 500 - 750
ZFUNC 510 - 700
ZWRITEENABLE 520 - 680
STENCILENABLE 540 - 650
STENCILPASS 560 - 630
SRCBLEND 500 - 685
Two_Sided_StencilMODE 450 - 590
DAPAT DIJADIKAN ALFATETENABLE 470 - 525
ALPHAREF 460 - 530
ALPHAFUNC 450 - 540
DESTBLEND 475 - 510
COLORWRITEENABLE 465 - 515
CCW_STENCILFAIL 340 - 560
CCW_STENCILPASS 340 - 545
CCW_STENCILZFAIL 330 - 495
SCISSORTESTENABLE 375 - 440
CCW_STENCILFUNC 250 - 480
SetScissorRect 150 - 340

 

Topik Tingkat Lanjut