Optimasi tanpa pengukuran hanyalah tebakan. Profiler memberikan pengukuran itu dalam bentuk daftar fungsi yang paling banyak menyita waktu, yang disebut hotspot. Topik ini memprofilkan satu program dengan dua kernel perkalian matriks menggunakan gprof, membandingkan dampak flag optimasi -O0 dan -O2, serta menjelaskan pola akses memori yang menjadi sebab selisihnya. Semua perintah dijalankan dari direktori topik ini, dan angka durasi yang disebut dalam prosa adalah hasil pengukuran pada lingkungan jalur ini yang akan berbeda pada mesin lain.
Program contoh dan dua kernel#
Program matriks.f90 mengisi dua matriks 300 x 300, lalu mengalikannya dengan dua kernel yang aritmetikanya sama tetapi urutan perulangannya berbeda. Kernel kalenaif memakai urutan i-j-k, sedangkan kalentata memakai urutan i-k-j. Matriks Fortran tersusun kolom-utama (column-major), yaitu elemen-elemen satu kolom menempati alamat memori yang berurutan, sehingga pada kalentata penulisan c(i,j) dan pembacaan b(k,j) bergerak menyusuri memori secara berurutan. Prosesor tidak menarik satu elemen sekaligus melainkan satu baris cache (cache line) yang memuat beberapa elemen berurutan, sehingga perulangan yang menyusuri memori mendayagunakan setiap tarikan itu sementara perulangan yang melompat membuang sebagian besar isinya. Dampak perbedaan urutan itu diukur pada bagian-bagian berikutnya.
program matriks
implicit none
integer, parameter :: n = 300
real :: a(n,n), b(n,n), c(n,n)
integer :: k
call isi(a)
call isi_b(b)
do k = 1, 8
call kalenaif(a, b, c)
end do
print '(a, f10.4)', "c(1,1) naif = ", c(1,1)
print '(a, f10.4)', "c(n,n) naif = ", c(n,n)
do k = 1, 8
call kalentata(a, b, c)
end do
print '(a, f10.4)', "c(1,1) tata = ", c(1,1)
print '(a, f10.4)', "c(n,n) tata = ", c(n,n)
contains
subroutine isi(m)
real, intent(out) :: m(:,:)
integer :: i, j
do j = 1, size(m, 2)
do i = 1, size(m, 1)
m(i,j) = real(mod(i + j, 7)) * 0.25
end do
end do
end subroutine isi
subroutine isi_b(m)
real, intent(out) :: m(:,:)
integer :: i, j
do j = 1, size(m, 2)
do i = 1, size(m, 1)
m(i,j) = real(mod(i * j, 11)) * 0.5
end do
end do
end subroutine isi_b
subroutine kalenaif(a, b, c)
real, intent(in) :: a(:,:), b(:,:)
real, intent(out) :: c(:,:)
integer :: i, j, k
c = 0.0
do i = 1, n
do j = 1, n
do k = 1, n
c(i,j) = c(i,j) + a(i,k) * b(k,j)
end do
end do
end do
end subroutine kalenaif
subroutine kalentata(a, b, c)
real, intent(in) :: a(:,:), b(:,:)
real, intent(out) :: c(:,:)
integer :: i, j, k
c = 0.0
do i = 1, n
do k = 1, n
do j = 1, n
c(i,j) = c(i,j) + a(i,k) * b(k,j)
end do
end do
end do
end subroutine kalentata
end program matriksDampak flag optimasi#
Flag -O0 membangun program dengan penerjemahan paling langsung, sedangkan -O2 mengaktifkan pengoptimalan aras prosedur seperti penyusunan ulang perulangan dan penyisipan fungsi. Blok berikut membangun versi -O2, menjalankannya, lalu mengukur durasi kedua versi dengan time; pesan time sengaja tidak dijadikan keluaran karena nilainya berubah antareksekusi.
# bangun versi -O2 dan jalankan
gfortran -Wall -Wextra -O2 matriks.f90 -o matriks-O2
./matriks-O2c(1,1) naif = 558.2500
c(n,n) naif = 565.1250
c(1,1) tata = 558.2500
c(n,n) tata = 565.1250# bangun versi tanpa optimasi untuk pembanding
gfortran -O0 -g matriks.f90 -o matriks-O0
# ukur durasi kedua versi; pesan time dialihkan agar tidak menjadi keluaran
{ time ./matriks-O0 > /dev/null ; } 2> /dev/null
{ time ./matriks-O2 > /dev/null ; } 2> /dev/nullPada lingkungan yang dipakai di sini, versi -O0 selesai sekitar 3,1 detik sedangkan versi -O2 sekitar 0,27 detik, sehingga flag optimasi saja memberi percepatan sekitar sebelas kali tanpa satu baris kode pun diubah. Kedua versi menghasilkan nilai yang sama persis karena tingkat optimasi ini tidak mengubah urutan operasi aritmetikanya. Angka durasi bergantung pada mesin, tetapi selisih urutan besarnya tidak.
Membaca hotspot dengan gprof#
gprof memerlukan kompilasi dengan flag -pg, yang menyisipkan kode penghitung pemanggilan pada setiap fungsi. Program yang dijalankan sekali menghasilkan berkas gmon.out, dan gprof merangkumnya menjadi profil datar (flat profile) berupa tabel waktu per fungsi. Program dikompilasi -pg -O0 agar setiap prosedurnya masih berdiri sendiri, karena pada -O2 gfortran menyisipkan kedua kernel ke program utama sehingga keduanya hilang dari tabel. Tabel itu dibaca mulai dari kolom detik sendiri: fungsi dengan persentase waktu terbesar adalah sasaran pertama optimasi, dan jumlah pemanggilan menjelaskan apakah biayanya berasal dari sedikit panggilan berat atau banyak panggilan ringan.
# bangun versi terprofil, jalankan, lalu tampilkan nama fungsi dan jumlah panggilannya
gfortran -pg -O0 matriks.f90 -o matriks-pg
./matriks-pg > /dev/null
gprof -b matriks-pg gmon.out | awk 'NF==7 && $1+0==$1 {print $7, $4}' | sort
rm -f gmon.outMAIN__ 1
isi.3 1
isi_b.2 1
kalenaif.1 8
kalentata.0 8Tabel profil datar memuat kolom persentase waktu, detik kumulatif, detik sendiri, jumlah pemanggilan, dan nama fungsi. Ekstraksi di atas mengambil dua kolom yang stabil antareksekusi, yaitu nama dan jumlah pemanggilan, sedangkan kolom-kolom waktunya berubah dari satu eksekusi ke eksekusi lain. Akhiran .0 sampai .3 pada nama adalah pembeda yang dibuat gfortran untuk prosedur-prosedurnya, dan MAIN__ adalah nama program utama. Pada eksekusi yang dipakai di sini, kedua kernel dipanggil delapan kali dan keduanya menyerap hampir seluruh waktu program, sedangkan isi dan isi_b hanya dipanggil sekali dengan porsi waktu kecil. Daftar seperti ini memastikan usaha optimasi diarahkan ke kernel, bukan ke pengisian matriks.
Membaca cache dengan perf#
Perbedaan kalenaif dan kalentata bukan pada jumlah operasi, melainkan pada pola akses memori. Pada urutan i-j-k, setiap langkah k membuat akses a(i,k) melompat sejauh satu baris matriks di memori, sedangkan urutan i-k-j membuat penulisan c(i,j) dan pembacaan b(k,j) bergerak berurutan sepanjang kolom. Untuk matriks 300 x 300, lompatan itu berarti 1.200 byte per langkah karena melewati satu kolom penuh, jarak yang jauh melampaui ukuran satu baris cache sehingga hampir setiap akses memaksa tarikan memori baru. Dampak pola itu pada kejadian cache miss diukur dengan perf, yang tidak terpasang pada lingkungan jalur ini sehingga contohnya tidak dieksekusi di sini.
# tidak-dijalankan
# perf tidak terpasang pada lingkungan jalur ini
perf stat -e cache-references,cache-misses ./matriks-O2Pada lingkungan yang memilikinya, perf stat melaporkan jumlah kejadian cache di samping durasi, sedangkan perf record dan perf report menampilkan fungsi terpanas sampai ke tingkat baris sumber. Kesimpulannya sejalan dengan gprof: perbaikan performa numerik umumnya datang dari mengurangi pergerakan memori, bukan dari mengurangi jumlah operasi.
Kesalahan umum#
- Mengoptimalkan fungsi yang bukan hotspot. Usaha pada
isiyang porsinya kecil tidak memengaruhi durasi total. Baca profil dahulu, baru pilih sasarannya. - Membandingkan durasi dari satu kali eksekusi. Durasi satu kali berjalan dipengaruhi beban sistem, sehingga pengukuran diulang beberapa kali dan yang dibandingkan adalah selisih urutan besarnya.
- Menyimpulkan hasil dari bangun
-O0. Perubahan yang tampak mempercepat pada-O0bisa hilang pada-O2karena kompiler sudah menata ulang kodenya. Ukur pada flag yang akan dipakai produksi.
Ringkasan#
- Profiler mengubah optimasi dari tebakan menjadi pengukuran:
gprofdengan-pgmenampilkan tabel waktu dan jumlah panggilan per fungsi dari satu kali eksekusi. - Flag
-O2memberi percepatan sekitar sebelas kali pada program contoh tanpa mengubah hasil aritmetikanya. - Urutan perulangan
i-k-jlebih ramah cache daripadai-j-kpada matriks kolom-utama, danperfmengukur akibatnya pada kejadian cache bila tersedia.
Referensi#
- GNU gprof — GNU Project
— Manual gprof yang menjelaskan flat profile, call graph, dan opsi
-b. - The GNU Fortran Compiler — GNU Project
— Manual gfortran untuk opsi
-O,-pg, dan-g. - perf Tutorial — perf wiki
— Tutorial resmi alat perf untuk
perf statdanperf record.